﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Linq;
using XcoAppSpaces.Contracts.Transport;
using XcoAppSpaces.Contracts.Logging;
using XcoAppSpaces.Transport.Sockets.Auth.Contract;
using XcoAppSpaces.Transport.Sockets.Auth;

namespace XcoAppSpaces.Transport.Sockets
{
	internal class TCPServer : IDisposable
	{
        private readonly IXcoLogger log = null;

		private readonly TcpListener listener = null;
		private readonly XcoTCPTransportService commService;
		private bool running = false;
		
		private readonly Dictionary<string, TcpClient> activeClients = null;
		private readonly List<TcpClient> activeReceivers = new List<TcpClient>(); //list of active receivers, kept to be able to close all receivers when stopping the server
		
		private readonly bool connectionBidirectional = true;
	    private readonly TransferHelper transferHelper;

		private readonly SecuritySettings securitySettings;
		private readonly IAuthenticator authenticator;

        public TCPServer(ref int port, string ipAddressStr,
			XcoTCPTransportService commService, IXcoLogger log, SecuritySettings securitySettings)
		{
			this.commService = commService;
			this.log = log;
			this.securitySettings = securitySettings;
			this.authenticator = InitializeAuthenticator();
			
			transferHelper = new TransferHelper();
			connectionBidirectional = Config.ConnectionBidirectional;

			if (connectionBidirectional)
			{
				activeClients = new Dictionary<string, TcpClient>();
			}
			
			if (ipAddressStr == null)
			{
				//IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
				//IPAddress ipAddress = ipHostInfo.AddressList[0];
				listener = new TcpListener(IPAddress.Any, port);
			}
			else
			{
				listener = new TcpListener(IPAddress.Parse(ipAddressStr), port);
			}
			listener.Start();

			if (port == 0) //when a random port is used, ask the listener which port got assigned
				port = ((IPEndPoint)listener.LocalEndpoint).Port;

            running = true;
			(new Thread(Start)).Start();
		}

		public IPEndPoint LocalEndPoint
		{
			get { return (IPEndPoint)listener.LocalEndpoint; }
		}

		private IAuthenticator InitializeAuthenticator()
		{
			var authenticator = Authenticator.CreateDefaultInstance();
			authenticator.Inject(securitySettings);
			authenticator.Out_AuthenticatedConnection += client =>
			{
				log.Debug("Incoming connection from " + client.Client.RemoteEndPoint + " successfully authenticated");
				StartReceiverAction(() => StartReceive(client, null));
			};
			authenticator.Out_InvalidConnection += client =>
			{
				log.Warn("Warning: Unauthenticated connection attempt from " + client.Client.RemoteEndPoint);
				DisposeClient(client);
			};
			return authenticator;
		}

		public IEnumerable<string> ActiveConnections
		{
			get
			{
				if (activeClients == null)
					return null;
				lock (activeClients)
				{
					return activeClients.Keys.ToList();
				}
			}
		}

		public TcpClient GetActiveTcpClient(string remoteAddr)
		{
			TcpClient result = null;
			if (activeClients != null && remoteAddr != null)
			{
				lock (activeClients)
				{
					if (activeClients.ContainsKey(remoteAddr))
						result = activeClients[remoteAddr];
				}
			}
			return result;
	
		}
		public void AddReceiver(TcpClient client, Action refreshTimeout)
		{
			//Register client only for receiving
			StartReceiverAction(() => StartReceive(client, refreshTimeout));
		}
        
       /* public void LogCallStack(string label)
        {
            StackTrace stackTrace = new StackTrace();           // get call stack
            StackFrame[] stackFrames = stackTrace.GetFrames();  // get method calls (frames)
        
            // write call stack method names
            foreach (StackFrame stackFrame in stackFrames)
            {
                log.Error(label + ": " + stackFrame.GetMethod().ReflectedType.FullName + " -> " + stackFrame.GetMethod());   // write method name
            }
        }*/
		
		private void Start()
        {
            log.Debug("TCP Server started");
			while (running)
			{
				try
				{
					TcpClient client = listener.AcceptTcpClient();
					if (client == null)
						continue;
					lock (activeReceivers)
					{
						activeReceivers.Add(client);
					}
					ThreadPool.QueueUserWorkItem(_ => DoAuthentication(client));
				}
				catch (Exception ex)
				{
					if (running)
						log.Error("TCP Server Communication Error", ex);
				}
			}
            log.Debug("TCP Server closed");
		}

		private void DoAuthentication(TcpClient client)
		{
			if (securitySettings.AuthenticationKey == null)
				StartReceiverAction(() => StartReceive(client, null));
			else
			{
				log.Debug("Starting authentication on incoming connection");
				authenticator.In_Connection(client);
			}
		}

		private void StartReceive(TcpClient client, Action refreshClientTimeout)
		{
			bool addedToActiveClients = false;
			try
			{
                log.Debug("Starting to receive messages on new connection.");
				//Console.WriteLine("Client connected with IP {0}", ((IPEndPoint)client.Client.RemoteEndPoint).Address);

				var reader = new BinaryReader(client.GetStream());
				bool connectionOpen = true;
				while (connectionOpen)
				{
					string remoteAddress;
					XcoMessage msg = transferHelper.Convert(reader, out remoteAddress,
						client.Close,
						ex =>
						{
							if (ex is IOException || ex is ObjectDisposedException)
								connectionOpen = false;
							else
								log.Error("Error converting stream to XcoMessage. Error:", ex);
						});

                    if (msg != null)
                    {
						if (refreshClientTimeout != null)
							refreshClientTimeout();
                        log.Debug("New message received from " + remoteAddress);
						AddToActiveClients(client, remoteAddress, ref addedToActiveClients);
						commService.RaiseMessageReceivedEvent(msg, remoteAddress);
                    }
                    else
                    {
                        if (!client.Client.Connected)
                        {//connection closed by client
                            break;
                        }
                        if (!running)
                            break;
                    }
				}
			}
			catch (Exception ex)
			{
				if (running)
					log.Error("TCP Server Communication Error", ex);
			}
			finally
			{
				DisposeClient(client);
			}
		}

		private void DisposeClient(TcpClient client)
		{
			if (client.Client != null)
				client.Client.Close();
			client.Close();
			lock (activeReceivers)
			{
				activeReceivers.Remove(client);
			}
			RemoveFromActiveClients(client);
		}

		private void RemoveFromActiveClients(TcpClient client)
		{
			if (activeClients == null) 
				return;

			lock (activeClients)
			{
				foreach (var c in activeClients)
				{
					if (c.Value == client)
					{
						activeClients.Remove(c.Key);
						break;
					}
				}
			}
		}

		private void AddToActiveClients(TcpClient client, string remoteAddress, ref bool addedToActiveClients)
		{
			if (!connectionBidirectional || addedToActiveClients) 
				return;

			lock (activeClients)
			{
				TcpClient existingClient;
				if (activeClients.TryGetValue(remoteAddress, out existingClient) && existingClient != client)
				{
					existingClient.Client.Close();
					existingClient.Close();
				}
				activeClients[remoteAddress] = client;
			}
			addedToActiveClients = true;
		}

		private void StartReceiverAction(Action receiverAction)
		{
			new Thread(new ThreadStart(receiverAction)).Start(); //ThreadPool is not used because it is a long running task
		}

		public void Stop()
		{
            running = false;
			if (listener != null)
			{
				listener.Stop();
			}
			//close all incoming active connections
			lock (activeReceivers)
			{
				foreach (var receiver in activeReceivers)
				{
					receiver.Client.Close();
					receiver.Close();
				}
				activeReceivers.Clear();
			}
			if (activeClients != null)
			{
				lock (activeClients)
				{//already closed TCPClients
					activeClients.Clear();
				}
			}
		}

		public void Dispose()
		{
			Stop();
		}
	}
}
